package lumis.portal.url;

import lumis.portal.PortalException;
import lumis.portal.PortalObjectNotFoundException;
import lumis.portal.PortalRequestParameters;
import lumis.portal.authentication.SessionConfig;
import lumis.portal.cache.ICacheDataProvider;
import lumis.portal.cache.PortalCache;
import lumis.portal.manager.ManagerFactory;
import lumis.portal.manager.ManagerFactoryInternal;
import lumis.portal.page.PageConfig;
import lumis.portal.page.cache.PageCacheConfig;
import lumis.portal.transaction.ITransaction;
import lumis.portal.transaction.PortalTransactionFactory;
import lumis.portal.user.UserConfig;
import lumis.util.LocaleUtil;
import lumis.util.log.ILogger;
import lumis.util.log.LoggerFactory;

import java.lang.reflect.Field;
import java.lang.reflect.Modifier;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Locale;
import java.util.Set;
import java.util.concurrent.locks.Lock;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import javax.persistence.NoResultException;
import javax.persistence.PersistenceException;

import org.hibernate.CacheMode;

/**
 * {@link IURLManager} implementation.
 * @since 5.0.0
 * @version $Revision: 12710 $ $Date: 2011-03-18 17:00:16 -0300 (Fri, 18 Mar 2011) $
 */
public class URLManager implements IURLManager
{
	private static final HashMap<String, String> portalStudioRuntimeInterfacesByPageId = new HashMap<String, String>();
	static
	{
		portalStudioRuntimeInterfacesByPageId.put(PageConfig.PAGE_ID_ADMIN_SERVICES,
				"lumis.service.portalmanagement.serviceinstance.administration");

		portalStudioRuntimeInterfacesByPageId.put(PageConfig.PAGE_ID_ADMIN_PAGES, "lumis.service.portalmanagement.page.administration");

		portalStudioRuntimeInterfacesByPageId.put(PageConfig.PAGE_ID_ADMIN_SUBCHANNELS,
				"lumis.service.portalmanagement.channel.administration");

		portalStudioRuntimeInterfacesByPageId.put(PageConfig.PAGE_ID_ADMIN_CHANNEL_TEMPLATES,
				"lumis.service.portalmanagement.channeltemplate.administration");

		portalStudioRuntimeInterfacesByPageId.put(PageConfig.PAGE_ID_ADMIN_PAGE_TEMPLATES,
				"lumis.service.portalmanagement.pagetemplate.administration");

		portalStudioRuntimeInterfacesByPageId.put(PageConfig.PAGE_ID_ADMIN_LOCALUSERS,
				"lumis.service.portalmanagement.localuser.administration");

		portalStudioRuntimeInterfacesByPageId.put(PageConfig.PAGE_ID_ADMIN_LOCALGROUPS,
				"lumis.service.portalmanagement.localgroup.administration");
	}

	/**
	 * Class used in {@link URLManager#normalizeDynamicPath(String)}
	 * to represent a parameter in the dynamic path.
	 *
	 * @version $Revision: 12710 $ $Date: 2011-03-18 17:00:16 -0300 (Fri, 18 Mar 2011) $
	 * @since 5.0.0
	 */
	private static class DynamicPathParameter implements Comparable<DynamicPathParameter>
	{
		private final String parameterString;
		private final String parameterName;
		private final String parameterValue;
		private final int originalPosition;

		public DynamicPathParameter(String parameterString, int originalPosition)
		{
			this.parameterString = parameterString;
			String[] paramSplit = parameterString.split("=", 2);
			this.parameterName = paramSplit[0];
			if (paramSplit.length > 1)
				this.parameterValue = paramSplit[1];
			else
				this.parameterValue = null;
			this.originalPosition = originalPosition;
		}
		
		public int compareTo(DynamicPathParameter o)
		{
			int result = parameterName.compareTo(o.parameterName);
			if (result == 0)
				result = originalPosition - o.originalPosition;
			return result;
		}
	};
	
	private static ILogger logger = LoggerFactory.getLogger(URLManager.class);
	
	/**
	 * Pattern used to detect dynamic paths that should not be converted.
	 * Used in {@link #getWebResourceByDynamicPath(String, lumis.portal.url.IURLManager.CreateStrategy, Locale)}.
	 * @since 5.0.0 
	 */
	private static final Pattern PATTERN_PATHS_NOT_TO_CONVERT = Pattern.compile("(\\?|\\&(amp;)?)(" + 
			PortalRequestParameters.PAGE_PARAMETER_PREVIEW_ITEMID + "|" + 
			PortalRequestParameters.PAGE_PARAMETER_PROCESS_ACTION + "|" + 
			PortalRequestParameters.PAGE_PARAMETER_RUN_TIME_INTERFACE +")=");
	
	/**
	 * Contains the identifiers of administrative pages. These are the
	 * PAGE_ID_ADMIN_* constants in {@link PageConfig}.
	 * @since 5.0.0
	 */
	private static Set<String> ADMIN_PAGE_IDS = new HashSet<String>(16);
	static
	{
		for (Field field: PageConfig.class.getDeclaredFields())
		{
			if (field.getName().startsWith("PAGE_ID_ADMIN_") && (field.getModifiers() & Modifier.STATIC) > 0)
			{
				try
				{
					ADMIN_PAGE_IDS.add((String)field.get(null));
				}
				catch (Exception e)
				{
					// should not happen
					throw new RuntimeException(e);
				}
			}
		}
	}

	/**
	 * Encapsulation of cache key for {@link URLManager#PATH_CONVERSION_CACHE}.
	 * 
	 * @version $Revision: 12710 $ $Date: 2011-03-18 17:00:16 -0300 (Fri, 18 Mar 2011) $
	 * @since 5.0.0
	 */
	private static class PathConversionCacheKey
	{
		private final String dynamicPath;
		private final Locale locale;
		private final boolean convertGuestOnlyCaches;

		/**
		 * Generates a {@link PathConversionCacheKey} object from the format
		 * generated by {@link #toString()}.
		 * @param cacheKeyString the cache key in string format.
		 * @since 5.0.0
		 */
		public PathConversionCacheKey(String cacheKeyString)
		{
			convertGuestOnlyCaches = '1' == cacheKeyString.charAt(0);
			String[] parts = cacheKeyString.split("#", 2);
			try
			{
				locale = LocaleUtil.FromString(parts[0].substring(1));
			}
			catch (PortalException e)
			{
				throw new RuntimeException(e);
			}
			dynamicPath = parts[1];
		}
		
		public PathConversionCacheKey(String dynamicPath, Locale locale, boolean convertGuestOnlyCaches)
		{
			this.dynamicPath = dynamicPath;
			this.locale = locale;
			this.convertGuestOnlyCaches = convertGuestOnlyCaches;
		}

		/**
		 * Returns a string representation for this key. The format is
		 * <code>(guest?1:0) + locale + '#' + dynamicPath</code>.
		 * @since 5.0.0
		 */
		@Override
		public String toString()
		{
			return (convertGuestOnlyCaches ? "1" : "0") + locale.toString() + "#" + dynamicPath;
		}

		public String getDynamicPath()
		{
			return dynamicPath;
		}

		public Locale getLocale()
		{
			return locale;
		}

		public boolean isConvertGuestOnlyCaches()
		{
			return convertGuestOnlyCaches;
		}
	}

	/**
	 * Cache entry used to store a {@link WebResource} in 
	 * {@link URLManager#TRANSIENT_WEB_RESOURCE_BY_DYNAMIC_PATH_CACHE}.
	 * 
	 * @version $Revision: 12710 $ $Date: 2011-03-18 17:00:16 -0300 (Fri, 18 Mar 2011) $
	 * @since 5.0.0
	 */
	private static class WebResourceCacheEntry
	{
		private final WebResource webResource;
		private final CreateStrategy createStrategy;
		
		public WebResourceCacheEntry(WebResource webResource, CreateStrategy createStrategy)
		{
			if (webResource.getId() != null)
				throw new IllegalArgumentException("Persistent web resource cannot be cached");
			
			this.webResource = webResource;
			this.createStrategy = createStrategy;
		}

		/**
		 * Returns the web resource in this cache entry.
		 * @param requestedCreateStrategy the create strategy currently being
		 * requested, that trigger this cache read.
		 * @return the web resource, if it may be used complying to the
		 * requested create strategy; otherwise <code>null</code>.
		 * @since 5.0.0
		 */
		public WebResource getWebResource(CreateStrategy requestedCreateStrategy)
		{
			try
			{
				if (requestedCreateStrategy == CreateStrategy.AUTO)
				{
					// if the requested create strategy was AUTO, only use the cached
					// web resource if it was also generated during an AUTO strategy.
					// This is because if the web resource was generated using 
					// another strategy, it may be necessary to persist it now since
					// the requested strategy is AUTO.
					if (createStrategy == CreateStrategy.AUTO)
						return webResource.clone();
					else
						return null;
				}
				else
				{
					// if requested create strategy was not AUTO, may use any
					// web resource cached
					return webResource.clone();
				}
			}
			catch(CloneNotSupportedException e)
			{
				logger.warn("Exception thrown while getting web resource from cache", e);
				return null;
			}
		}
	}
	
	/**
	 * Caches dynamic path conversions.
	 * <p> 
	 * The key is generated using {@link PathConversionCacheKey},
	 * and the value is the converted path, or <code>null</code> if no
	 * conversion was necessary.
	 * @since 5.0.0
	 */
	private static PortalCache<String> PATH_CONVERSION_CACHE = new PortalCache<String>("lumis.portal.url.URLManager#pathConversion", new ICacheDataProvider<String>()
	{
		public String loadData(String key) throws PortalException
		{
			PathConversionCacheKey cacheKey = new PathConversionCacheKey(key);
			try
			{
				WebResource webResource = ManagerFactoryInternal.getURLManager().getWebResourceByDynamicPath(
							cacheKey.getDynamicPath(), CreateStrategy.AUTO, cacheKey.getLocale());

				String path = webResource.getPath(cacheKey.isConvertGuestOnlyCaches());
				if (path.equals(cacheKey.getDynamicPath()))
					return null;
				else
					return path;
			}
			catch(IllegalArgumentException e)
			{
				// if the dynamic path is invalid, it won't be converted
				return null;
			}
		}
	});
	
	/**
	 * Cache used to store transient results of 
	 * {@link #getWebResourceByDynamicPath(String, lumis.portal.url.IURLManager.CreateStrategy, Locale)}.
	 * <p>
	 * The key is "<code>&lt;locale&gt;#&lt;dynamicPath&gt;</code>".
	 * @since 5.0.0
	 */
	private static PortalCache<WebResourceCacheEntry> TRANSIENT_WEB_RESOURCE_BY_DYNAMIC_PATH_CACHE = new PortalCache<WebResourceCacheEntry>("lumis.portal.url.URLManager#webResourceByDynamicPath");
	
	public String getWebResourcePathByDynamicPath(String dynamicPath, Locale locale) throws PortalException
	{
		return getWebResourcePathByDynamicPath(dynamicPath, locale, UserConfig.USER_GUEST_ID.equals(SessionConfig.getCurrentSessionConfig()
				.getUserId()));
	}

	/**
	 * Pattern used to identify
	 * {@link PortalRequestParameters#PAGE_PARAMETER_PREVIOUS_STORAGE_ID
	 * previous storage identifier} in a dynamic path.
	 */
	private static final Pattern PATTERN_PSID_PARAM = Pattern
			.compile(PortalRequestParameters.PAGE_PARAMETER_PREVIOUS_STORAGE_ID + "=[^&]+");

	/**
	 * Returns a converted path of a {@link WebResource} by it's dynamic path,
	 * locale and considering or not guest only cache.
	 * 
	 * @param dynamicPath the dynamic path.
	 * @param locale the locale.
	 * @param considerGuestOnlyCache indicates if it should or shouldn't consider guest only cache.
	 * @return the converted path.
	 * @since 5.6.0
	 */
	private String getWebResourcePathByDynamicPath(String dynamicPath, Locale locale, boolean considerGuestOnlyCache)
			throws PortalException
	{
		ArrayList<String> psIds = new ArrayList<String>(2);

		Matcher matcher = PATTERN_PSID_PARAM.matcher(dynamicPath);
		while (matcher.find())
		{
			String psId = matcher.group();
			psIds.add(psId);
		}

		PathConversionCacheKey cacheKey = new PathConversionCacheKey(normalizeDynamicPath(dynamicPath), locale,
				considerGuestOnlyCache);
		String convertedPath = PATH_CONVERSION_CACHE.fetch(cacheKey.toString());

		if (convertedPath != null)
			for (String psId : psIds)
			{
				if (convertedPath.contains("?"))
					convertedPath += "&";
				else
					convertedPath += "?";

				convertedPath += psId;
			}

		return convertedPath != null ? convertedPath : dynamicPath;
	}
	
	public WebResource getWebResourceByDynamicPath(String dynamicPath, 
			CreateStrategy createStrategy) throws PortalException
	{
		SessionConfig sessionConfig = SessionConfig.getCurrentSessionConfig();
		return getWebResourceByDynamicPath(dynamicPath, createStrategy, 
				sessionConfig.getLocale());
	}
	
	public WebResource getWebResourceByDynamicPath(String dynamicPath, 
			CreateStrategy createStrategy, Locale locale) throws PortalException
	{
		dynamicPath = normalizeDynamicPath(dynamicPath);
		
		// check if there is an applicable cached transient web resource
		final String cacheEntryKey = locale.toString() + "#" + dynamicPath;
		WebResourceCacheEntry webResourceCacheEntry = TRANSIENT_WEB_RESOURCE_BY_DYNAMIC_PATH_CACHE.get(cacheEntryKey);
		if (webResourceCacheEntry != null)
		{
			WebResource webResource = webResourceCacheEntry.getWebResource(createStrategy);
			if (webResource != null)
				return webResource;
		}

		// variables used on multiple blocks below
		final boolean convert = !PATTERN_PATHS_NOT_TO_CONVERT.matcher(dynamicPath).find();
		IWebResourceFactory factory;
		WebResource webResource;
		final boolean cacheEnabled;
		final String friendlyPath;
		
		ITransaction txCreate = PortalTransactionFactory.createTransaction();
		try
		{
			txCreate.begin();
			
			if (convert)
			{
				// try to get web resource from persistence
				try
				{
					webResource = (WebResource) ManagerFactory.getEntityManager().createNamedQuery(WebResource.NAMED_QUERY_FIND_BY_DYNAMIC_PATH_AND_LOCALE)
							.setParameter("dynamicPath", dynamicPath)
							.setParameter("locale", locale)
							.getSingleResult();
					
					// commit transaction in progress and return the 
					// web resource found in persistence
					txCreate.commit();
					return webResource;
				}
				catch (NoResultException e)
				{
					// just continue below, to create the web resource
				}
			}
		
			if (!createStrategy.shouldCreateNewInstance())
				throw new PortalObjectNotFoundException("WebResource for dynamicPath '" + dynamicPath + "' not found");
			
			// create web resource
			factory = getWebResourceFactoryByDynamicPath(dynamicPath);
			webResource = factory.createWebResourceByDynamicPath(dynamicPath);
			webResource.setDynamicPath(dynamicPath);
			webResource.setLocale(locale);
			
			if (convert)
			{
				// set friendly path
				friendlyPath = resolveFriendlyPath(factory, webResource);
				webResource.setFriendlyPath(friendlyPath);
				
				// read web resource cache information
				cacheEnabled = factory.isCacheEnabled(webResource);
			}
			else
			{
				friendlyPath = null;
				cacheEnabled = false;
			}
			
			txCreate.commit();
		}
		finally
		{
			txCreate.dispose();
		}

		// friendly path clash adjustment and web resource persist must
		// be executed in a cluster-wide synchronized block. But it is 
		// necessary only if web resource will be persisted
		final boolean shouldPersist = createStrategy.shouldPersist(friendlyPath != null, cacheEnabled) && 
				webResource.mayBePersisted();
		if (shouldPersist)
		{
			try
			{
				Lock lock = startFriendlyPathClashCheck(friendlyPath);
				try
				{
					ITransaction txPersist = PortalTransactionFactory.createTransaction();
					try
					{
						txPersist.begin();

						// adjust friendly path for clash
						if (friendlyPath != null)
							webResource.setFriendlyPath(checkAndAdjustFriendlyPathForClash(factory, friendlyPath));

						// persist web resource
						ManagerFactory.getEntityManager().persist(webResource);

						if (cacheEnabled)
						{
							// set web resource cache
							PageCacheConfig pageCacheConfig = new PageCacheConfig(webResource);
							factory.setPageCacheFilePath(pageCacheConfig);
							webResource.setPageCache(pageCacheConfig);
							ManagerFactory.getEntityManager().persist(pageCacheConfig);
						}

						ManagerFactory.getEntityManager().flush();
						txPersist.commit();
					}
					finally
					{
						txPersist.dispose();
					}
				}
				finally
				{
					endFriendlyPathClashCheck(lock);
				}
			}
			catch(PersistenceException e)
			{
				// this may be caused because of simultaneous add, try to get again
				// this depends on the unique index in lum_WebResource to prevent repeated adds
				ITransaction txGet2 = PortalTransactionFactory.createTransaction();
				try
				{
					txGet2.begin();

					try
					{
						webResource = (WebResource) ManagerFactory.getEntityManager().createNamedQuery(
								WebResource.NAMED_QUERY_FIND_BY_DYNAMIC_PATH_AND_LOCALE)
								.setParameter("dynamicPath", webResource.getDynamicPath())
								.setParameter("locale", webResource.getLocale())
								.setHint("org.hibernate.cacheMode", CacheMode.PUT)
								.getSingleResult();
					}
					catch (NoResultException e2)
					{
						// not found, so add did not fail because of simultaneous adds 
						// throw original exception
						throw e;
					}			

					txGet2.commit();
				}
				finally
				{
					txGet2.dispose();
				}
			}
		}
		
		if (cacheEnabled && webResource.getPageCache() == null)
		{
			ITransaction txCache = PortalTransactionFactory.createTransaction();
			try
			{
				txCache.begin();
				
				// set web resource cache
				PageCacheConfig pageCacheConfig = new PageCacheConfig(webResource);
				factory.setPageCacheFilePath(pageCacheConfig);
				webResource.setPageCache(pageCacheConfig);
				
				if (shouldPersist)
					ManagerFactory.getEntityManager().persist(pageCacheConfig);
				
				txCache.commit();
			}
			finally
			{
				txCache.dispose();
			}
		}
		
		// if the web resource instance is transient
		if (webResource.getId() == null)
		{
			// special treatment for some Portal Studio special pages. They
			// now require the runTimeInterface but it is injected here for
			// better compatibility and no need to change the links to such pages
			final String pageId = webResource.getParameter(PortalRequestParameters.PAGE_PARAMETER_PAGEID);
			String rti = webResource.getParameter(PortalRequestParameters.PAGE_PARAMETER_RUN_TIME_INTERFACE);
			if(pageId != null && rti == null)
			{
				rti = portalStudioRuntimeInterfacesByPageId.get(pageId);
				if(rti != null)
				{
					webResource.setDynamicPath(normalizeDynamicPath(webResource.getDynamicPath() + 
							"&" + PortalRequestParameters.PAGE_PARAMETER_RUN_TIME_INTERFACE + "=" + rti));
				}
			}
			
			// cache only if does not have a page cache. Don't want to handle
			// related entity dirty issues
			if (webResource.getPageCache() == null)
			{
				// generate web resource path to be pre-calculated in cache
				ITransaction txPath = PortalTransactionFactory.createTransaction();
				try
				{
					txPath.begin();
					webResource.getPath();
					txPath.commit();
				}
				finally
				{
					txPath.dispose();
				}
				
				// store web resource in cache
				WebResourceCacheEntry cacheEntry = new WebResourceCacheEntry(webResource, createStrategy);
				TRANSIENT_WEB_RESOURCE_BY_DYNAMIC_PATH_CACHE.put(cacheEntryKey, cacheEntry);
			}
		}
		
		return webResource;
	}
	
	/**
	 * Pattern used to obtain the friendly path lock identifier in 
	 * {@link #startFriendlyPathClashCheck(String)}.
	 * @since 5.0.0
	 */
	private static final Pattern PATTERN_FRIENDLY_PATH_LOCK_ID = Pattern.compile("^(.*?)(?:-\\d+)?(?:\\..*)?$");

	/**
	 * Treats synchronization issues, and should be used around calls to 
	 * {@link #checkAndAdjustFriendlyPathForClash(IWebResourceFactory, String)}
	 * and the flush of the change to the database.
	 * <p>
	 * {@link #endFriendlyPathClashCheck(Lock)} must be called to release
	 * the lock returned.
	 * @return the lock, or <code>null</code> if no lock is needed.
	 * @see #endFriendlyPathClashCheck(Lock)
	 * @since 5.0.0
	 */
	private Lock startFriendlyPathClashCheck(String friendlyPath)
	{
		if (friendlyPath == null)
			return null;
		
		// generate lock identifier for the given friendly path
		Matcher matcher = PATTERN_FRIENDLY_PATH_LOCK_ID.matcher(friendlyPath);
		matcher.matches();
		String friendlyPathLockId = "lumis.portal.url.URLManager#webResource#" + matcher.group(1);
		if (friendlyPathLockId.length() > 255)
			friendlyPathLockId = friendlyPathLockId.substring(0, 255);
		
		// perform the lock
		Lock lock = ManagerFactory.getLockManager().getLock(friendlyPathLockId);
		lock.lock();
		return lock;
	}
	
	/**
	 * Ends the friendly path clash check started by 
	 * {@link #startFriendlyPathClashCheck(String)}.
	 * @param lock the lock returned by {@link #startFriendlyPathClashCheck(String)}.
	 * @see #startFriendlyPathClashCheck(String)
	 * @since 5.0.0
	 */
	private void endFriendlyPathClashCheck(Lock lock)
	{
		if (lock != null)
			lock.unlock();
	}
	
	/**
	 * Normalizes a dynamic path. Removes anchor and reorders the parameters.
	 * @param dynamicPath the dynamic path.
	 * @return the normalized dynamic path.
	 * @since 5.0.0
	 */
	private String normalizeDynamicPath(String dynamicPath)
	{
		// remove anchor
		int anchorIndex = dynamicPath.indexOf("#");
		if (anchorIndex > -1)
			dynamicPath = dynamicPath.substring(0, anchorIndex);
		
		// remove ending '?'
		if (dynamicPath.endsWith("?"))
			dynamicPath = dynamicPath.substring(0, dynamicPath.length()-1);
		
		// reorder dynamic path parameters
		String[] arrDynamicPath = dynamicPath.split("\\?", 2);
		if (arrDynamicPath.length > 1)
		{
			boolean removeChannelId = false;
			
			// generate a list with the parameters sorted
			String[] parameterStrs = arrDynamicPath[1].split("&");
			List<DynamicPathParameter> parameters = new ArrayList<DynamicPathParameter>(parameterStrs.length);
			int position = 0;
			for (String parameterStr : parameterStrs)
			{
				DynamicPathParameter parameter = new DynamicPathParameter(parameterStr, position++);
				if(parameter.parameterValue == null || parameter.parameterValue.length() == 0)
					continue;
				parameters.add(parameter);
				
				// if pageId is not for admin page, remove channelId from dynamic path (below)
				if (PortalRequestParameters.PAGE_PARAMETER_PAGEID.equals(parameter.parameterName))
				{
					if (!ADMIN_PAGE_IDS.contains(parameter.parameterValue))
						removeChannelId = true;
				}
			}
			Collections.sort(parameters);
			
			// recreate the dynamic path
			StringBuilder newDynamicPath = new StringBuilder(dynamicPath.length());
			newDynamicPath.append(arrDynamicPath[0]).append("?");
			for (Iterator<DynamicPathParameter> itParameters = parameters.iterator(); itParameters.hasNext();)
			{
				DynamicPathParameter parameter = itParameters.next();
				
				// ignore channelId parameter if set to remove it above
				if (removeChannelId && PortalRequestParameters.PAGE_PARAMETER_CHANNELID.equals(parameter.parameterName))
					continue;
				
				// ignore lumPSId
				if (PortalRequestParameters.PAGE_PARAMETER_PREVIOUS_STORAGE_ID.equals(parameter.parameterName))
					continue;

				// append parameter
				newDynamicPath.append(parameter.parameterString);
				if (itParameters.hasNext())
					newDynamicPath.append("&");
			}
			dynamicPath = newDynamicPath.toString();
		}
		
		return dynamicPath;
	}

	/**
	 * Resolves the friendly path for a web resource. This method does not
	 * adjust the friendly path for clashes.
	 * @param webResourceFactory the web resource factory.
	 * @param webResource the web resource.
	 * @return the friendly path, or <code>null</code> if resolution was for
	 * no friendly path.
	 * @since 5.0.0
	 */
	private String resolveFriendlyPath(IWebResourceFactory webResourceFactory, WebResource webResource) throws PortalException
	{
		// get the URL resolvers
		List<IURLResolver> resolvers;
		try
		{
			resolvers = getURLResolvers(webResource);
		}
		catch (Exception e)
		{
			logger.error("Error during creation of URL resolvers for " + webResource, e);
			return null;
		}

		if (logger.isDebugEnabled())
			logger.debug("Starting friendlyPath resolution for " + webResource);

		// perform friendlyPath resolution
		String friendlyPath = "";
		try
		{
			for (IURLResolver resolver: resolvers)
			{
				if (logger.isDebugEnabled())
					logger.debug("Before calling IURLResolver '" + resolver + "', friendlyPath = '" + friendlyPath + "'");
				
				friendlyPath = resolver.resolveFriendlyPath(webResource, friendlyPath);
			}
		}
		catch (Exception e)
		{
			logger.error("Error during resolution of friendlyPath for " + webResource, e);
			return null;
		}
		
		if (logger.isDebugEnabled())
			logger.debug(webResource + " resolved to: '" + friendlyPath + "'");
		
		// friendlyPath resolution cannot be null
		if (friendlyPath == null)
		{
			logger.error("Friendly path resolution for " + webResource + " generated a null value.");
			return null;
		}
		
		// an empty string corresponds to no friendly path
		if (friendlyPath.length() == 0)
			return null;
		
		// friendlyPath must start with '/'
		if (friendlyPath.charAt(0) != '/')
		{
			logger.error("Friendly path resolved for " + webResource + " did not start with '/'. It was: '" + friendlyPath + "'");
			return null;
		}
		
		String adjustedFriendlyPath = webResourceFactory.adjustResolvedFriendlyPath(webResource, friendlyPath);
		if (logger.isDebugEnabled())
			logger.debug("Friendly path for " + webResource + " adjusted from '" + friendlyPath + "' to '" + adjustedFriendlyPath + "'");
		
		return adjustedFriendlyPath;
	}

	/**
	 * Checks if a friendly path clash occurred, and generates a new friendly
	 * path if necessary.
	 * @param factory the web resource's factory.
	 * @param friendlyPath the friendly path.
	 * @return the friendly path with the clash resolved. If there was no
	 * clash, the original friendly path is returned.
	 * @see #startFriendlyPathClashCheck(String)
	 * @since 5.0.0
	 */
	private String checkAndAdjustFriendlyPathForClash(IWebResourceFactory factory, String friendlyPath)
	{
		if (friendlyPath == null)
			return null;
		
		int friendlyPathSuffix = 0;
		String result = friendlyPath;
		
		boolean friendlyPathIsUnique = false;
		while (!friendlyPathIsUnique)
		{
			// search for clash
			int count = ManagerFactory.getEntityManager().createNamedQuery(WebResource.NAMED_QUERY_FIND_BY_FRIENDLY_PATH)
					.setParameter("friendlyPath", result)
					.getResultList().size();
			if (count > 0)
			{
				// clash has happened, adjust friendly path
				friendlyPathSuffix++;
				result = factory.adjustFriendlyPathForClash(friendlyPath, result, friendlyPathSuffix); 
			}
			else
			{
				// no clash
				friendlyPathIsUnique = true;
			}
		}
		
		return result;
	}

	/**
	 * Returns the URL resolvers for a web resource.
	 * <p>
	 * If friendly URL is disabled, according to {@link IURLManager#FRIENDLY_URL_ENABLE_PROPERTY_NAME},
	 * an empty list is returned.
	 * <p>
	 * The URL resolver class names are obtained from {@link IURLManager#URL_RESOLVER_PROPERTY_NAME}.
	 * 
	 * @param webResource the web resource.
	 * @return a list with the URL resolvers.
	 * 
	 * @throws InstantiationException  if a URL resolver class represents an 
	 * abstract class, an interface, an array class, a primitive type, or void; 
	 * or if the class has no nullary constructor; or if the instantiation fails for some other reason.
	 * @throws IllegalAccessException if a URL resolver class or its nullary constructor is not accessible.
	 * @throws ClassNotFoundException if a URL resolver class cannot be located.
	 * 
	 * @since 5.0.0
	 */
	private List<IURLResolver> getURLResolvers(WebResource webResource) 
			throws InstantiationException, IllegalAccessException, ClassNotFoundException
	{
		// check if friendly URL is enabled
		boolean friendlyURLEnabled;
		List<String> friendlyURLEnableValues = webResource.getProperties().get(FRIENDLY_URL_ENABLE_PROPERTY_NAME);
		if (friendlyURLEnableValues == null || friendlyURLEnableValues.isEmpty())
			friendlyURLEnabled = false;
		else
			friendlyURLEnabled = Boolean.parseBoolean(friendlyURLEnableValues.iterator().next());
		
		// if friendly URL is disabled, there are no resolvers for the given web resource
		if (!friendlyURLEnabled)
			return Collections.emptyList();
		
		// read URL resolvers class names
		List<String> urlResolverClassNames = webResource.getProperties().get(URL_RESOLVER_PROPERTY_NAME);
		
		// if URL resolvers not specified, default is GenericURLResolver
		if (urlResolverClassNames == null || urlResolverClassNames.isEmpty())
			return Collections.<IURLResolver>singletonList(new GenericURLResolver());
		
		// create URL resolvers
		List<IURLResolver> resolvers = new ArrayList<IURLResolver>(urlResolverClassNames.size());
		for (String urlResolverClassName: urlResolverClassNames)
		{
			IURLResolver urlResolver = (IURLResolver)ManagerFactory.getDeploymentManager().getClassLoader().loadClass(urlResolverClassName).newInstance();
			resolvers.add(urlResolver);
		}
		return resolvers;
	}

	/**
	 * Pattern used to detect pageId parameter in {@link #getWebResourceFactoryByDynamicPath(String)}.
	 * @since 5.0.0
	 */
	private static Pattern PATTERN_PAGE_ID_PARAMETER = Pattern.compile("[\\?&]" + PortalRequestParameters.PAGE_PARAMETER_PAGEID + "=");
	
	/**
	 * Returns the web resource factory to be used for a dynamic path.
	 * @param dynamicPath the dynamic path.
	 * @return the web resource factory.
	 * @throws IllegalArgumentException if could not resolve a web resource 
	 * factory for the given dynamic path.
	 * 
	 * @since 5.0.0
	 */
	private IWebResourceFactory getWebResourceFactoryByDynamicPath(String dynamicPath)
	{
		if (!dynamicPath.startsWith("/" + PageConfig.PAGE_MAIN))
			throw new IllegalArgumentException("Could not resolve web resource factory for dynamic path: " + dynamicPath);
		
		if (PATTERN_PAGE_ID_PARAMETER.matcher(dynamicPath).find())
			return (IWebResourceFactory)ManagerFactory.getPageManager();
		else
			return (IWebResourceFactory)ManagerFactory.getChannelManager();
	}

	/**
	 * Pattern that finds links to other pages. 
	 * Used in {@link #convertHTMLPathsToDynamicPaths(String)}.
	 * @since 5.0.0
	 */
	private static final Pattern PATTERN_PAGE_LINK = Pattern.compile("((action|href)\\s*=\\s*\\\\?['\"])([^\\\\'\"#\\?]+)([^\\\\'\"]*)(?=\\\\?['\"])");
	
	public String convertHTMLPathsToDynamicPaths(String html) throws PortalException
	{
		Matcher matcher = PATTERN_PAGE_LINK.matcher(html);
		
		StringBuffer convertedHtml = new StringBuffer(html.length() * 2);
		while (matcher.find())
		{
			String href = matcher.group(3);
			
			// do not convert absolute references
			if (href.indexOf(':') == -1 || href.charAt(0) == '/')
			{
				// ignore references to dynamic paths
				if (href.startsWith(PageConfig.PAGE_MAIN))
					continue;
				
				// get referenced web resource
				String path = "/" + href;
				WebResource webResource = getWebResourceByPath(path);
				
				if (webResource != null)
				{
					// the suffix is usually parameters and/or anchor
					String suffix = matcher.group(4);
					
					// do the replacement
					matcher.appendReplacement(convertedHtml, Matcher.quoteReplacement(
							matcher.group(1) + webResource.getDynamicPath().substring(1) + suffix));
				}
			}
		}
		matcher.appendTail(convertedHtml);
		
		String result = convertedHtml.toString();
		
		if(result.contains("lumDoNotConvertDynUrls"))
			result = result.replace("#lumDoNotConvertDynUrls-", "").replace("-lumDoNotConvertDynUrls#", "");
		
		return result;
	}

	/**
	 * Returns the web resource with the specified path. The path may be a 
	 * friendly path or a cache file path.
	 * @param path the path.
	 * @return the web resource, or <code>null</code> if it was not found.
	 * @since 5.0.0
	 */
	private WebResource getWebResourceByPath(String path) throws PortalException
	{
		if (path.charAt(path.length()-1) == '/')
			path = path.substring(0, path.length()-1);
			
		try
		{
			// search for friendly path matching the given path
			return (WebResource)ManagerFactory.getEntityManager().createNamedQuery(
					WebResource.NAMED_QUERY_FIND_BY_FRIENDLY_PATH)
					.setParameter("friendlyPath", path)
					.getSingleResult();
		}
		catch (NoResultException e)
		{
			try
			{
				// search for page cache matching the given path
				return (WebResource)ManagerFactory.getEntityManager().createNamedQuery(
						WebResource.NAMED_QUERY_FIND_BY_FILE_PATH)
						.setParameter("filePath", path)
						.getSingleResult();
			}
			catch (NoResultException e1)
			{
				return null;
			}
		}
	}

	public String processHTMLDynamicPathReferences(String html) throws PortalException
	{
		SessionConfig sessionConfig = SessionConfig.getCurrentSessionConfig();
		return processHTMLDynamicPathReferences(html, sessionConfig.getLocale(), 
				UserConfig.USER_GUEST_ID.equals(sessionConfig.getUserId()));
	}

	/**
	 * Pattern used to find references to main.jsp in HTML code, excluding form actions.
	 * Used in {@link #processHTMLDynamicPathReferences(String, Locale, boolean)}.
	 * @since 5.0.0
	 */
	private static final Pattern PATTERN_PAGE_MAIN_EXCLUDING_ACTION = Pattern.compile("(\"|')(" + PageConfig.PAGE_MAIN + "\\?[^#\"']*)");
	
	public String processHTMLDynamicPathReferences(String htmlContent,
			Locale locale, boolean convertGuestOnlyCaches)
			throws PortalException
	{
		Matcher matcher = PATTERN_PAGE_MAIN_EXCLUDING_ACTION.matcher(htmlContent);

		StringBuffer convertedHtml = new StringBuffer(htmlContent.length());
		while (matcher.find())
		{
			String dynamicPath = "/" + matcher.group(2).replace("&amp;", "&");
			String convertedPath = getWebResourcePathByDynamicPath(dynamicPath, locale, convertGuestOnlyCaches);
			if (!dynamicPath.equals(convertedPath))
				matcher.appendReplacement(convertedHtml, Matcher.quoteReplacement(matcher.group(1) + convertedPath.substring(1)));
		}
		matcher.appendTail(convertedHtml);
		
		String result = convertedHtml.toString();
		
		if(result.contains("lumDoNotConvertDynUrls"))
			result = result.replace("#lumDoNotConvertDynUrls-", "").replace("-lumDoNotConvertDynUrls#", "");

		return result;
	}

	/**
	 * Clears {@link URLManager}'s internal caches containing web resource 
	 * related information.
	 * 
	 * @since 5.0.0
	 */
	void clearWebResourceCaches()
	{
		PATH_CONVERSION_CACHE.clear();
		TRANSIENT_WEB_RESOURCE_BY_DYNAMIC_PATH_CACHE.clear();
	}

	public void setWebResourcesMissingData(IWebResourceFactory webResourceFactory,
			Collection<WebResource> webResources, boolean cacheEnabled, 
			boolean friendlyURLEnabled) throws PortalException
	{
		for (WebResource webResource: webResources)
		{
			ITransaction transaction = PortalTransactionFactory.createTransaction();
			try
			{
				transaction.begin();
				
				webResource = ManagerFactory.getEntityManager().find(WebResource.class, webResource.getId());
				if (friendlyURLEnabled && webResource.getFriendlyPath() == null)
				{
					// set friendly path
					String friendlyPath = resolveFriendlyPath(webResourceFactory, webResource);
					if (friendlyPath != null)
					{
						Lock lock = startFriendlyPathClashCheck(friendlyPath);
						try
						{
							friendlyPath = checkAndAdjustFriendlyPathForClash(webResourceFactory, friendlyPath);
							webResource.setFriendlyPath(friendlyPath);
							ManagerFactory.getEntityManager().flush();
						}
						finally
						{
							endFriendlyPathClashCheck(lock);
						}
					}
				}
				
				if (cacheEnabled)
				{
					if (webResource.getPageCache() != null)
					{
						webResourceFactory.setPageCacheFilePath(webResource.getPageCache());
					}
					else
					{
						// set web resource cache
						PageCacheConfig pageCacheConfig = new PageCacheConfig(webResource);
						webResourceFactory.setPageCacheFilePath(pageCacheConfig);
						webResource.setPageCache(pageCacheConfig);
						
						// persist pageCacheConfig
						ManagerFactory.getEntityManager().persist(pageCacheConfig);
					}
				}
				
				transaction.commit();
			}
			finally
			{
				transaction.dispose();
			}
		}
	}
	
	private static final Pattern PATTERN_VALUE_PAGE_MAIN = Pattern.compile("((value)\\s*=\\s*(\"|'))(?=" + PageConfig.PAGE_MAIN + "|lumis/|data/)");
	private static final Pattern PATTERN_ATTRIBS_MISC = Pattern.compile("((src|background|action|href)\\s*=\\s*(\"|'|\\\\\"|\\\\'|(?=lumis|data)))(?!\\s*http|\\s*/|\\s*javascript|\\s*news:|\\s*ftp:|\\s*file:|#|mailto:|\\\\\\\\)");
	private static final Pattern PATTERN_JAVASCRIPT_MISC = Pattern.compile("((location\\.replace|location\\.assign|window\\.open)\\s*\\(\\s*(\"|'|&quot;))(?!\\s*http|\\s*/|\\s*javascript|\\s*news:|\\s*ftp:|\\s*file:|#|mailto:|\\\\\\\\)");
	private static final Pattern PATTERN_VALUE_DATA_DOC = Pattern.compile("(value\\s*=\\s*(\"|'))data/document/");
	private static final Pattern PATTERN_BACKGROUND_URL = Pattern.compile("(background:url\\()(?!http)");
	private static final String tempPlaceHolder = "*(2,!";
	
	public String fixReferencesAccordingToPath(String html, String path) throws PortalException
	{
		int lastSlashIndex = path.lastIndexOf('/');
		if (lastSlashIndex == 0)
			return html;
		String folderPath = path.substring(1, lastSlashIndex);
		
		String[] folderItems = folderPath.split("/");
		StringBuilder upToContext = new StringBuilder(5+(folderItems.length*3));
		
		for(int i = 0; i<folderItems.length; i++)
			upToContext.append("../");
		
		upToContext.append(tempPlaceHolder);
		String upToContextString = upToContext.toString();
		
		html = PATTERN_VALUE_PAGE_MAIN.matcher(html).replaceAll("$1"+upToContextString);
		html = PATTERN_ATTRIBS_MISC.matcher(html).replaceAll("$1"+upToContextString);
		html = PATTERN_JAVASCRIPT_MISC.matcher(html).replaceAll("$1"+upToContextString);
		html = PATTERN_VALUE_DATA_DOC.matcher(html).replaceAll("$1"+upToContextString+"data/document/");
		html = PATTERN_BACKGROUND_URL.matcher(html).replaceAll("$1"+upToContextString);

		String curPath = upToContextString + path;

		html = html.replace(curPath+"/", "");
		for(int i = folderItems.length-1; i > 0; i--)
		{
			curPath = curPath.substring(3, curPath.length() - folderItems[i].length()-1);
			html = html.replace(curPath+"/", "");
		}
		
		html = html.replace(tempPlaceHolder, "");
		
		return html;
	}
	
	/**
	 * Clears the page cache reference in a web resource.
	 * @param webResource the web resource.
	 * @see WebResource#pageCache
	 * @since 5.0.0
	 */
	public void clearPageCacheReference(WebResource webResource)
	{
		webResource.clearPageCacheReference();
	}
}